package com.grouuu.core;

import com.grouuu.event.TimerEvent;
import com.grouuu.factory.SpriteFactory;
import com.grouuu.keyboard.IKey;
import com.grouuu.keyboard.KeyListener;
import com.grouuu.Map;
import com.grouuu.mouse.IMouse;
import com.grouuu.mouse.Mouse;
import com.grouuu.tile.TileGraphic;
import com.grouuu.update.ITimer;
import com.grouuu.update.Timer;
import flash.ui.Keyboard;
import nme.geom.Point;
import starling.core.Starling;
import starling.display.BlendMode;
import starling.display.Image;
import starling.display.Quad;
import starling.display.QuadBatch;
import starling.display.Sprite;
import starling.display.Stage;
import starling.events.Event;
import starling.events.KeyboardEvent;
import starling.events.Touch;
import starling.events.TouchEvent;
import starling.events.TouchPhase;

/**
 * ...
 * @author Grouuu
 */

class Scene extends Sprite, implements ITimer, implements IMouse, implements IKey
{
	static inline var TILE_WIDTH:Int = 16;
	static inline var TILE_HEIGHT:Int = 16;
	static inline var NB_TILE_MARGIN:Int = 2;
	static inline var MOVE_SPEED:Float = 5;
	
	private var starling:Starling;
	private var factorySprite:SpriteFactory;
	private var timer:Timer;
	private var keyboard:KeyListener;
	private var mouse:Mouse;
	
	private var stage_W:Int;
	private var stage_H:Int;
	private var nbCol:Int;
	private var nbRow:Int;
	
	public function new() 
	{
		super();
		
		addEventListener(Event.ADDED_TO_STAGE, onAdded);
	}
	
	private function onAdded(event:Event):Void
	{
		init();
	}
	
	private function init():Void
	{
		stage_W = Starling.current.stage.stageWidth;
		stage_H = Starling.current.stage.stageHeight;
		
		nbCol = Std.int(stage_W / TILE_WIDTH);
		nbRow = Std.int(stage_H / TILE_HEIGHT);
		
		starling = Starling.current;
		
		starling.showStats = true;
		
		factorySprite = new SpriteFactory();
		timer = new Timer();
		keyboard = new KeyListener();
		mouse = new Mouse();
		
		quadTiles = new QuadBatch();
		
		timer.addListener(this);
		keyboard.addListener(this);
		mouse.addListener(this);
		
		timer.start();
		
		initMap();
		initHero();
		
		parseTile(updateTiles);
	}
	
	// INIT ///////////////////////////////////////////////////////////////////////////////////
	
	private var map:Array<Array<Int>>;
	private var quadTiles:QuadBatch;
	private var renderList:flash.Vector<TileGraphic>;
	private var originTileX:Int;
	private var originTileY:Int;
	private var limitMinX:Int;
	private var limitMaxX:Int;
	private var limitMinY:Int;
	private var limitMaxY:Int;
	private var edit:Bool;
	private var centerPoint:Point;
	private var decalX:Float;
	private var decalY:Float;
	private var lockX:Bool;
	private var lockY:Bool;
	private var globalDesti:Point;
	
	private function initMap():Void
	{
		map = Map.DATA;
		
		originTileX = Scene.NB_TILE_MARGIN;
		originTileY = Scene.NB_TILE_MARGIN;
		
		limitMinX = -((map[0].length - nbCol) * Scene.TILE_WIDTH) + Scene.NB_TILE_MARGIN * Scene.TILE_WIDTH;
		limitMaxX = -Scene.NB_TILE_MARGIN * Scene.TILE_WIDTH;
		
		limitMinY = -((map.length - nbRow) * Scene.TILE_HEIGHT) + Scene.NB_TILE_MARGIN * Scene.TILE_HEIGHT;
		limitMaxY = -Scene.NB_TILE_MARGIN * Scene.TILE_HEIGHT;
		
		centerPoint = new Point(stage_W / 2, stage_H / 2);
		
		renderList = new flash.Vector<TileGraphic>((nbRow + Scene.NB_TILE_MARGIN * 2) * (nbCol + Scene.NB_TILE_MARGIN * 2));
		
		quadTiles = new QuadBatch();
		
		quadTiles.blendMode = BlendMode.NONE;
		
		quadTiles.x = limitMaxX;
		quadTiles.y = limitMaxY;
		
		addChild(quadTiles);
		
		edit = false;
		
		setRenderTileOrigin();
		
		parseTile(initRenderList);
		
		decalX = 0;
		decalY = 0;
		lockX = false;
		lockY = false;
		globalDesti = new Point();
	}
	
	private var hero:Quad;
	
	private function initHero():Void
	{
		hero = new Quad(16, 16, 0xFF0000);
		
		hero.pivotX = hero.width / 1;
		hero.pivotY = hero.height / 1;
		
		hero.x = starling.stage.stageWidth / 2;
		hero.y = starling.stage.stageHeight / 2;
		
		addChild(hero);
	}
	
	// TILES //////////////////////////////////////////////////////////////////////////////////
	
	private var indexRender:Int;
	private var idTile:Int;
	private var tileGraphic:TileGraphic;
	private var img:Image;
	private var posX:Int;
	private var posY:Int;
	private var renderTileX:Int;
	private var renderTileY:Int;
	private var renderTileMaxX:Int;
	private var renderTileMaxY:Int;
	
	private function parseTile(action:Int->Int->Int->Void):Void
	{
		var row:Int;
		var col:Int;
		
		for (row in renderTileY...renderTileMaxY - 1)
		{
			for (col in renderTileX...renderTileMaxX - 1)
			{
				indexRender = getIndexRender(row - renderTileY, col - renderTileX);
				
				action(indexRender, row, col);
			}
		}
	}
	
	private function initRenderList(indexRender:Int, row:Int, col:Int):Void
	{
		idTile = map[row][col];
		
		tileGraphic = factorySprite.getTileGraphic(idTile);
		
		renderList[indexRender] = tileGraphic;
	}
	
	private function updateTiles(indexRender:Int, row:Int, col:Int):Void
	{
		if (edit)
		{
			tileGraphic = renderList[indexRender];
		}
		else
		{
			idTile = map[row][col];
			
			tileGraphic = factorySprite.getTileGraphic(idTile);
			
			factorySprite.deleteTileGraphic(renderList[indexRender]);
			
			renderList[indexRender] = tileGraphic;
		}
		
		posX = Scene.TILE_WIDTH * col;
		posY = Scene.TILE_HEIGHT * row;
		
		img = tileGraphic.image();
		
		img.x = posX;
		img.y = posY;
		
		quadTiles.addImage(img);
	}
	
	// UTILS //////////////////////////////////////////////////////////////////////////////////
	
	private function getIndexRender(row:Int, col:Int):Int
	{
		return (row * (nbCol + Scene.NB_TILE_MARGIN * 2)) + col;
	}
	
	private function getRenderTileX(col:Int):Int
	{
		return col - Scene.NB_TILE_MARGIN;
	}
	
	private function getRenderTileY(row:Int):Int
	{
		return row - Scene.NB_TILE_MARGIN;
	}
	
	private function setRenderTileOrigin():Void
	{
		renderTileX = getRenderTileX(originTileX);
		renderTileY = getRenderTileY(originTileY);
		
		if (renderTileX < 0)
			renderTileX = 0;
		
		if (renderTileX > map[0].length - nbCol - Scene.NB_TILE_MARGIN * 2)
			renderTileX = map[0].length - nbCol - Scene.NB_TILE_MARGIN * 2;
		
		if (renderTileY < 0)
			renderTileY = 0;
		
		if (renderTileY > map.length - nbRow - Scene.NB_TILE_MARGIN * 2)
			renderTileY = map.length - nbRow - Scene.NB_TILE_MARGIN * 2;
		
		renderTileMaxX = renderTileX + nbCol + Scene.NB_TILE_MARGIN * 2;
		renderTileMaxY = renderTileY + nbRow + Scene.NB_TILE_MARGIN * 2;
	}
	
	// MOVE ///////////////////////////////////////////////////////////////////////////////////
	
	private var nbStep:Int;
	private var stepMoveX:Float;
	private var stepMoveY:Float;
	
	private function setMove(globalPoint:Point):Void
	{
		var localPoint:Point = quadTiles.globalToLocal(globalPoint);
		var heroPoint:Point = quadTiles.globalToLocal(new Point(hero.x, hero.y));
		
		var distance:Float = Math.sqrt((heroPoint.x - localPoint.x) * (heroPoint.x - localPoint.x) + (heroPoint.y - localPoint.y) * (heroPoint.y - localPoint.y));
		
		nbStep = Std.int(distance / Scene.MOVE_SPEED);
		
		stepMoveX = (localPoint.x - heroPoint.x) / nbStep;
		stepMoveY = (localPoint.y - heroPoint.y) / nbStep;
	}
	
	private var moveX:Float;
	private var moveY:Float;
	
	private function move():Void
	{
		moveX = stepMoveX;
		moveY = stepMoveY;
		
		if (!checkImpassable())
		{
			nbStep = 0;
			return;
		}
		
		moveMap();
		moveHero();
		updateOriginTiles();
		
		nbStep--;
		
		if (nbStep == 0)
		{
			quadTiles.x = Std.int(quadTiles.x);
			quadTiles.y = Std.int(quadTiles.y);
		}
	}
	
	private function checkImpassable():Bool
	{
		var boundHeroX:Float = (moveX >= 0) ? hero.width / 2 : -hero.width / 2;
		var boundHeroY:Float = (moveY >= 0) ? hero.height / 2 : -hero.height / 2;
		
		var mapPos:Point = quadTiles.globalToLocal(new Point(hero.x + moveX + boundHeroX, hero.y + moveY + boundHeroY));
		
		var idTileX:Int = Std.int(mapPos.x / Scene.TILE_WIDTH);
		var idTileY:Int = Std.int(mapPos.y / Scene.TILE_HEIGHT);
		
		idTileX -= getRenderTileX(originTileX);
		idTileY -= getRenderTileY(originTileY);
		
		indexRender = getIndexRender(idTileY, idTileX);
		
		tileGraphic = renderList[indexRender];
		
		return tileGraphic.isPassable();
	}
	
	private var oldPosMapX:Float;
	private var oldPosMapY:Float;
	
	private function moveMap():Void
	{
		oldPosMapX = quadTiles.x;
		oldPosMapY = quadTiles.y;
		
		if (lockX && lockY)
			return;
		
		if (!lockX)
		{
			quadTiles.x -= moveX;
			
			if (quadTiles.x < limitMinX)
			{
				quadTiles.x = limitMinX;
				moveX -= oldPosMapX - limitMinX;
				lockX = true;
			}
			else if (quadTiles.x > limitMaxX)
			{
				quadTiles.x = limitMaxX;
				moveX += limitMaxX - oldPosMapX;
				lockX = true;
			}
		}
		
		if (!lockY)
		{
			quadTiles.y -= moveY;
			
			if (quadTiles.y < limitMinY)
			{
				quadTiles.y = limitMinY;
				moveY -= oldPosMapY - limitMinY;
				lockY = true;
			}
			else if (quadTiles.y > limitMaxY)
			{
				quadTiles.y = limitMaxY;
				moveY += limitMaxY - oldPosMapY;
				lockY = true;
			}
		}
	}
	
	private var oldPosHeroX:Float;
	private var oldPosHeroY:Float;
	
	private function moveHero():Void
	{
		oldPosHeroX = hero.x;
		oldPosHeroY = hero.y;
		
		if (lockX)
		{
			hero.x += moveX;
			
			if (oldPosHeroX < centerPoint.x && hero.x >= centerPoint.x)
			{
				hero.x = centerPoint.x;
				moveX += oldPosHeroX - centerPoint.x;
				quadTiles.x -= moveX;
				lockX = false;
			}
			else if (oldPosHeroX > centerPoint.x && hero.x <= centerPoint.x)
			{
				hero.x = centerPoint.x;
				moveX -= centerPoint.x - oldPosHeroX;
				quadTiles.x += moveX;
				lockX = false;
			}
		}
		
		if (lockY)
		{
			hero.y += moveY;
			
			if (oldPosHeroY < centerPoint.y && hero.y >= centerPoint.y)
			{
				hero.y = centerPoint.y;
				moveY += oldPosHeroY - centerPoint.y;
				quadTiles.y -= moveY;
				lockY = false;
			}
			else if (oldPosHeroY > centerPoint.y && hero.y <= centerPoint.y)
			{
				hero.y = centerPoint.y;
				moveY -= centerPoint.y - oldPosHeroY;
				quadTiles.y += moveY;
				lockY = false;
			}
		}
	}
	
	private function updateOriginTiles():Void
	{
		decalX += quadTiles.x - oldPosMapX;
		decalY += quadTiles.y - oldPosMapY;
		
		if (decalX < -Scene.TILE_WIDTH)
		{
			decalX += Scene.TILE_WIDTH;
			originTileX++;
			edit = true;
		}
		else if (decalX > Scene.TILE_WIDTH)
		{
			decalX -= Scene.TILE_WIDTH;
			originTileX--;
			edit = true;
		}
		
		if (decalY < -Scene.TILE_HEIGHT)
		{
			decalY += Scene.TILE_HEIGHT;
			originTileY++;
			edit = true;
		}
		else if (decalY > Scene.TILE_HEIGHT)
		{
			decalY -= Scene.TILE_HEIGHT;
			originTileY--;
			edit = true;
		}
	}
	
	// TIMER //////////////////////////////////////////////////////////////////////////////////
	
	public function update(event:TimerEvent):Void
	{
		quadTiles.reset();
		
		if (nbStep > 0)
		{
			move();
		}
		
		if (edit)
		{
			setRenderTileOrigin();
		}
		
		parseTile(updateTiles);
		
		edit = false;
	}
	
	public function start():Void{}
	public function stop():Void{}
	
	// KEY ////////////////////////////////////////////////////////////////////////////////////
	
	private var keyLeft:Bool;
	private var keyRight:Bool;
	private var keyTop:Bool;
	private var keyBottom:Bool;
	
	public function onKeyDown(event:KeyboardEvent):Void
	{
		switch(event.keyCode)
		{
			case Keyboard.LEFT : 	keyLeft = 	true;
			case Keyboard.RIGHT : 	keyRight = 	true;
			case Keyboard.UP : 		keyTop = 	true;
			case Keyboard.DOWN : 	keyBottom = true;
		}
	}
	
	private var increment:Int = 50;
	
	public function onKeyUp(event:KeyboardEvent):Void
	{
		switch(event.keyCode)
		{
			case Keyboard.LEFT : 	keyLeft = 	false;
			case Keyboard.RIGHT : 	keyRight = 	false;
			case Keyboard.UP : 		keyTop = 	false;
			case Keyboard.DOWN : 	keyBottom = false;
		}
		
		switch(event.keyCode)
		{
			case Keyboard.LEFT :
				setMove(new Point(hero.x - increment, hero.y));
			case Keyboard.RIGHT :
				setMove(new Point(hero.x + increment, hero.y));
			case Keyboard.UP :
				setMove(new Point(hero.x, hero.y - increment));
			case Keyboard.DOWN :
				setMove(new Point(hero.x, hero.y + increment));
		}
	}
	
	// MOUSE //////////////////////////////////////////////////////////////////////////////////
	
	private  var isMouseDown:Bool = false;
	
	public function onClick(event:TouchEvent):Void
	{
		var touch:Touch = event.getTouch(this);
		
		if (touch == null)
			return;
		
		if (touch.phase == TouchPhase.BEGAN)
			isMouseDown = true;
		
		if (touch.phase == TouchPhase.ENDED)
			isMouseDown = false;
		
		if (isMouseDown)
		{
			globalDesti.x = touch.globalX;
			globalDesti.y = touch.globalY;
			
			setMove(globalDesti);
		}
	}
}